/**
 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "plugins/ecmascript/runtime/base/builtins_base.h"
#include "plugins/ecmascript/runtime/ecma_vm.h"
#include "plugins/ecmascript/runtime/global_env.h"
#include "plugins/ecmascript/runtime/internal_call_params.h"
#include "plugins/ecmascript/runtime/js_invoker.h"
#include "plugins/ecmascript/runtime/js_set.h"
#include "plugins/ecmascript/runtime/js_set_iterator.h"
#include "plugins/ecmascript/runtime/linked_hash_table-inl.h"
#include "plugins/ecmascript/runtime/object_factory.h"
#include "plugins/ecmascript/runtime/tagged_array-inl.h"

namespace ark::ecmascript::builtins {
JSTaggedValue set::SetConstructor(EcmaRuntimeCallInfo *argv)
{
    ASSERT(argv);
    BUILTINS_API_TRACE(argv->GetThread(), Set, SetConstructor);
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);
    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    // 1.If NewTarget is undefined, throw a TypeError exception
    JSHandle<JSTaggedValue> newTarget = builtins_common::GetNewTarget(argv);
    if (newTarget->IsUndefined()) {
        // throw type error
        THROW_TYPE_ERROR_AND_RETURN(thread, "new target can't be undefined", JSTaggedValue::Exception());
    }
    // 2.Let set be OrdinaryCreateFromConstructor(NewTarget, "%SetPrototype%", «‍[[SetData]]» ).
    JSHandle<JSTaggedValue> constructor = builtins_common::GetConstructor(argv);
    JSHandle<JSObject> obj = factory->NewJSObjectByConstructor(JSHandle<JSFunction>(constructor), newTarget);
    // 3.returnIfAbrupt()
    RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
    JSHandle<JSSet> set = JSHandle<JSSet>::Cast(obj);
    // 3.ReturnIfAbrupt(set).
    // 4.Set set’s [[SetData]] internal slot to a new empty List.
    JSHandle<LinkedHashSet> linkedSet = LinkedHashSet::Create(thread);
    set->SetLinkedSet(thread, linkedSet);

    // add data into set from iterable
    // 5.If iterable is not present, let iterable be undefined.
    // 6.If iterable is either undefined or null, let iter be undefined.
    JSHandle<JSTaggedValue> iterable(builtins_common::GetCallArg(argv, 0));
    // 8.If iter is undefined, return set
    if (iterable->IsUndefined() || iterable->IsNull()) {
        return set.GetTaggedValue();
    }
    // Let adder be Get(set, "add").
    JSHandle<JSTaggedValue> adderKey(factory->NewFromCanBeCompressString("add"));
    JSHandle<JSTaggedValue> setHandle(set);
    JSHandle<JSTaggedValue> adder = JSObject::GetProperty(thread, setHandle, adderKey).GetValue();
    // ReturnIfAbrupt(adder).
    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, adder.GetTaggedValue());
    // If IsCallable(adder) is false, throw a TypeError exception
    if (!adder->IsCallable()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "adder is not callable", adder.GetTaggedValue());
    }
    // Let iter be GetIterator(iterable).
    JSHandle<JSTaggedValue> iter(JSIterator::GetIterator(thread, iterable));
    // ReturnIfAbrupt(iter).
    RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, iter.GetTaggedValue());
    // values in iterator_result may be a JSArray, values[0] = key values[1]=value, used valueIndex to get value from
    // jsarray
    JSHandle<JSTaggedValue> valueIndex(thread, JSTaggedValue(1));
    JSHandle<JSTaggedValue> next = JSIterator::IteratorStep(thread, iter);

    while (!next->IsFalse()) {
        // ReturnIfAbrupt(next).
        RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, next.GetTaggedValue());
        // Let nextValue be IteratorValue(next).
        JSHandle<JSTaggedValue> nextValue(JSIterator::IteratorValue(thread, next));
        // ReturnIfAbrupt(nextValue).
        RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, nextValue.GetTaggedValue());
        auto info = NewRuntimeCallInfo(thread, adder, set, JSTaggedValue::Undefined(), 1);
        if (nextValue->IsArray(thread)) {
            auto prop = JSObject::GetProperty(thread, nextValue, valueIndex).GetValue();
            info->SetCallArgs(prop);
        } else {
            info->SetCallArgs(nextValue);
        }
        JSTaggedValue ret = JSFunction::Call(info.Get());
        // Let status be Call(adder, set, «nextValue.[[value]]»).
        JSHandle<JSTaggedValue> status(thread, ret);

        if (UNLIKELY(thread->HasPendingException())) {
            return JSIterator::IteratorCloseAndReturn(thread, iter, status);
        }
        // Let next be IteratorStep(iter).
        next = JSIterator::IteratorStep(thread, iter);
    }
    return set.GetTaggedValue();
}

JSTaggedValue set::proto::Add(EcmaRuntimeCallInfo *argv)
{
    ASSERT(argv);
    BUILTINS_API_TRACE(argv->GetThread(), SetPrototype, Add);
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);
    JSHandle<JSTaggedValue> self = builtins_common::GetThis(argv);

    // 2.If Type(S) is not Object, throw a TypeError exception.
    // 3.If S does not have a [[SetData]] internal slot, throw a TypeError exception.
    if (!self->IsJSSet()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "obj is not JSSet", JSTaggedValue::Exception());
    }

    JSHandle<JSTaggedValue> value(builtins_common::GetCallArg(argv, 0));
    JSHandle<JSSet> set(JSTaggedValue::ToObject(thread, self));

    JSSet::Add(thread, set, value);
    return set.GetTaggedValue();
}

JSTaggedValue set::proto::Clear(EcmaRuntimeCallInfo *argv)
{
    ASSERT(argv);
    BUILTINS_API_TRACE(argv->GetThread(), SetPrototype, Clear);
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);
    JSHandle<JSTaggedValue> self = builtins_common::GetThis(argv);

    // 2.If Type(S) is not Object, throw a TypeError exception.
    // 3.If S does not have a [[SetData]] internal slot, throw a TypeError exception.
    if (!self->IsJSSet()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "obj is not JSSet", JSTaggedValue::Exception());
    }
    JSHandle<JSSet> set(thread, JSSet::Cast(*JSTaggedValue::ToObject(thread, self)));
    JSSet::Clear(thread, set);
    return JSTaggedValue::Undefined();
}

JSTaggedValue set::proto::Delete(EcmaRuntimeCallInfo *argv)
{
    ASSERT(argv);
    BUILTINS_API_TRACE(argv->GetThread(), SetPrototype, Delete);
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);
    JSHandle<JSTaggedValue> self = builtins_common::GetThis(argv);
    // 2.If Type(S) is not Object, throw a TypeError exception.
    // 3.If S does not have a [[SetData]] internal slot, throw a TypeError exception.
    if (!self->IsJSSet()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "obj is not JSSet", JSTaggedValue::Exception());
    }

    JSHandle<JSSet> set(thread, JSSet::Cast(*JSTaggedValue::ToObject(thread, self)));
    JSHandle<JSTaggedValue> value = builtins_common::GetCallArg(argv, 0);
    bool flag = JSSet::Delete(thread, set, value);
    return builtins_common::GetTaggedBoolean(flag);
}

JSTaggedValue set::proto::Has(EcmaRuntimeCallInfo *argv)
{
    ASSERT(argv);
    BUILTINS_API_TRACE(argv->GetThread(), SetPrototype, Has);
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);
    JSHandle<JSTaggedValue> self = builtins_common::GetThis(argv);
    // 2.If Type(S) is not Object, throw a TypeError exception.
    // 3.If S does not have a [[SetData]] internal slot, throw a TypeError exception.
    if (!self->IsJSSet()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "obj is not JSSet", JSTaggedValue::Exception());
    }
    JSHandle<JSSet> jsSet(thread, JSSet::Cast(*JSTaggedValue::ToObject(thread, self)));
    JSHandle<JSTaggedValue> value = builtins_common::GetCallArg(argv, 0);
    bool flag = false;
    if (JSSet::IsKey(value.GetTaggedValue())) {
        int hash = LinkedHash::Hash(value.GetTaggedValue());
        flag = jsSet->Has(value.GetTaggedValue(), hash);
    }
    return builtins_common::GetTaggedBoolean(flag);
}

JSTaggedValue set::proto::ForEach([[maybe_unused]] EcmaRuntimeCallInfo *argv)
{
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);
    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    JSHandle<JSTaggedValue> self = builtins_common::GetThis(argv);
    // 2.If Type(S) is not Object, throw a TypeError exception.
    // 3.If S does not have a [[SetData]] internal slot, throw a TypeError exception.
    if (!self->IsJSSet()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "obj is not JSSet", JSTaggedValue::Exception());
    }
    JSHandle<JSSet> set(thread, JSSet::Cast(*JSTaggedValue::ToObject(thread, self)));

    // 4.If IsCallable(callbackfn) is false, throw a TypeError exception.
    JSHandle<JSTaggedValue> func(builtins_common::GetCallArg(argv, 0));
    if (!func->IsCallable()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "callbackfn is not callable", JSTaggedValue::Exception());
    }

    // 5.If thisArg was supplied, let T be thisArg; else let T be undefined.
    JSHandle<JSTaggedValue> thisArg = builtins_common::GetCallArg(argv, 1);

    // composed arguments
    JSHandle<JSTaggedValue> iter(factory->NewJSSetIterator(set, IterationKind::KEY));
    JSHandle<JSTaggedValue> result = JSIterator::IteratorStep(thread, iter);

    while (!result->IsFalse()) {
        RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, result.GetTaggedValue());
        JSHandle<JSTaggedValue> value = JSIterator::IteratorValue(thread, result);
        // Let funcResult be Call(callbackfn, T, «e, e, S»).
        auto info = NewRuntimeCallInfo(thread, func, thisArg, JSTaggedValue::Undefined(), 3);
        info->SetCallArgs(value, value, JSHandle<JSTaggedValue>(set));
        JSTaggedValue ret = JSFunction::Call(info.Get());  // 3: three args
        // returnIfAbrupt
        RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, ret);
        result = JSIterator::IteratorStep(thread, iter);
    }
    return JSTaggedValue::Undefined();
}

JSTaggedValue set::GetSpecies([[maybe_unused]] EcmaRuntimeCallInfo *argv)
{
    return builtins_common::GetThis(argv).GetTaggedValue();
}

JSTaggedValue set::proto::GetSize(EcmaRuntimeCallInfo *argv)
{
    ASSERT(argv);
    BUILTINS_API_TRACE(argv->GetThread(), SetPrototype, GetSize);
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);
    JSHandle<JSTaggedValue> self(builtins_common::GetThis(argv));
    // 2.If Type(S) is not Object, throw a TypeError exception.
    // 3.If S does not have a [[SetData]] internal slot, throw a TypeError exception.
    if (!self->IsJSSet()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "obj is not JSSet", JSTaggedValue::Exception());
    }
    JSSet *jsSet = JSSet::Cast(*JSTaggedValue::ToObject(thread, self));
    int count = jsSet->GetSize();
    return JSTaggedValue(count);
}

JSTaggedValue set::proto::Entries(EcmaRuntimeCallInfo *argv)
{
    ASSERT(argv);
    BUILTINS_API_TRACE(argv->GetThread(), SetPrototype, Entries);
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);
    JSHandle<JSTaggedValue> self = builtins_common::GetThis(argv);
    JSHandle<JSTaggedValue> iter = JSSetIterator::CreateSetIterator(thread, self, IterationKind::KEY_AND_VALUE);
    return iter.GetTaggedValue();
}

JSTaggedValue set::proto::Values(EcmaRuntimeCallInfo *argv)
{
    ASSERT(argv);
    BUILTINS_API_TRACE(argv->GetThread(), SetPrototype, Values);
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);
    JSHandle<JSTaggedValue> self = builtins_common::GetThis(argv);
    JSHandle<JSTaggedValue> iter = JSSetIterator::CreateSetIterator(thread, self, IterationKind::VALUE);
    return iter.GetTaggedValue();
}
}  // namespace ark::ecmascript::builtins
